pintobyte rngit
xous-core/services/shellchat/src/main.rs main (17e4bce8) Text, 19.63 KB
#![cfg_attr(target_os = "none", no_std)]
#![cfg_attr(target_os = "none", no_main)]
/*! Detailed docs are parked under Modules/cmds "Shell Chat" below
To make your own command, copy the \\`ver.rs\\` template (for an example with argument parsing),
or snag the very simple echo template below, and put
it in the services/shellchat/src/cmds/ directory:
\\`\\`\\`Rust
use crate::{ShellCmdApi, CommonEnv};
use String;
#[derive(Debug)]
pub struct Echo {
}
impl<'a> ShellCmdApi<'a> for Echo {
cmd_api!(echo); // inserts boilerplate for command API
fn process(&mut self, args: String, _env: &mut CommonEnv) -> Result<Option<String>, xous::Error> {
Ok(Some(rest))
}
}
\\`\\`\\`
Once you've added your command to the directory, go to the \\`cmds.rs\\` file, and follow
the four-step instructions embedded within the file, starting around line 40.
Check for more detailed docs under Modules/cmds "Shell Chat" below
*/
use core::fmt::Write;
use core::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use gam::UxRegistration;
use log::info;
use ux_api::minigfx::*;
use ux_api::service::api::*;
use xous::MessageEnvelope;
use xous_ipc::Buffer;
#[doc = include_str!("../README.md")]
mod cmds;
use cmds::*;
#[cfg(not(feature = "no-codec"))]
mod oqc_test;
#[cfg(feature = "nettest")]
mod nettests;
#[cfg(feature = "tracking-alloc")]
use std::alloc::System;
#[cfg(target_os = "xous")] // only draw "please wait" when not in hosted mode
use locales::t;
#[cfg(feature = "tracking-alloc")]
use tracking_allocator::{AllocationGroupId, AllocationTracker, Allocator};
#[cfg(feature = "tts")]
use tts_frontend::*;
#[cfg(feature = "tracking-alloc")]
#[global_allocator]
static GLOBAL: Allocator<System> = Allocator::system();
#[cfg(feature = "tracking-alloc")]
use core::sync::atomic::AtomicIsize;
#[cfg(feature = "tracking-alloc")]
struct StdoutTracker {
pub total: AtomicIsize,
}
#[cfg(feature = "tracking-alloc")]
impl AllocationTracker for StdoutTracker {
fn allocated(&self, addr: usize, size: usize, group_id: AllocationGroupId) {
// Allocations have all the pertinent information upfront, which you may or may not want to store for
// further analysis. Notably, deallocations also know how large they are, and what group ID
// they came from, so you typically don't have to store much data for correlating
// deallocations with their original allocation.
self.total.store(self.total.load(Ordering::SeqCst) + size as isize, Ordering::SeqCst);
println!(
"allocation -> total={} addr=0x{:0x} size={} group_id={:?}",
self.total.load(Ordering::SeqCst),
addr,
size,
group_id
);
}
fn deallocated(
&self,
addr: usize,
size: usize,
source_group_id: AllocationGroupId,
current_group_id: AllocationGroupId,
) {
// When a deallocation occurs, as mentioned above, you have full access to the address, size of the
// allocation, as well as the group ID the allocation was made under _and_ the active
// allocation group ID.
//
// This can be useful beyond just the obvious "track how many current bytes are allocated by the
// group", instead going further to see the chain of where allocations end up, and so on.
self.total.store(self.total.load(Ordering::SeqCst) - size as isize, Ordering::SeqCst);
println!(
"deallocation -> total={} addr=0x{:0x} size={} source_group_id={:?} current_group_id={:?}",
self.total.load(Ordering::SeqCst),
addr,
size,
source_group_id,
current_group_id
);
}
}
#[derive(Debug)]
struct History {
// the history record
pub text: String,
// if true, this was input from the user; if false, it's a response from the shell
pub is_input: bool,
}
#[allow(dead_code)]
struct Repl {
// optional structures that indicate new input to the Repl loop per iteration
// an input string
input: Option<String>,
// messages from other servers
msg: Option<MessageEnvelope>,
// record our input history
history: Vec<History>,
history_len: usize,
content: Gid,
gam: gam::Gam,
// variables that define our graphical attributes
screensize: Point,
bubble_width: u16,
margin: Point, // margin to edge of canvas
bubble_margin: Point, // margin of text in bubbles
bubble_radius: u16,
bubble_space: isize, // spacing between text bubbles
// command environment
env: CmdEnv,
// our security token for making changes to our record on the GAM
token: [u32; 4],
#[cfg(feature = "tts")]
tts: TtsFrontend,
}
impl Repl {
fn new(xns: &xous_names::XousNames, sid: xous::SID) -> Self {
let gam = gam::Gam::new(xns).expect("can't connect to GAM");
let token = gam
.register_ux(UxRegistration {
app_name: String::from(gam::APP_NAME_SHELLCHAT),
ux_type: gam::UxType::Chat,
#[cfg(not(feature = "tts"))]
predictor: Some(String::from(ime_plugin_shell::SERVER_NAME_IME_PLUGIN_SHELL)),
#[cfg(feature = "tts")]
predictor: Some(String::from(ime_plugin_tts::SERVER_NAME_IME_PLUGIN_TTS)),
listener: sid.to_array(), /* note disclosure of our SID to the GAM -- the secret is now
* shared with the GAM! */
redraw_id: ShellOpcode::Redraw.to_u32().unwrap(),
gotinput_id: Some(ShellOpcode::Line.to_u32().unwrap()),
audioframe_id: None,
rawkeys_id: None,
focuschange_id: Some(ShellOpcode::ChangeFocus.to_u32().unwrap()),
})
.expect("couldn't register Ux context for shellchat");
let content = gam.request_content_canvas(token.unwrap()).expect("couldn't get content canvas");
log::trace!("content canvas {:?}", content);
let screensize = gam.get_canvas_bounds(content).expect("couldn't get dimensions of content canvas");
log::trace!("size {:?}", screensize);
Repl {
input: None,
msg: None,
history: Vec::new(),
history_len: 10,
content,
gam,
screensize,
bubble_width: ((screensize.x / 5) * 4) as u16, // 80% width for the text bubbles
margin: Point::new(4, 4),
bubble_margin: Point::new(4, 4),
bubble_radius: 4,
bubble_space: 4,
env: CmdEnv::new(xns),
token: token.unwrap(),
#[cfg(feature = "tts")]
tts: TtsFrontend::new(xns).unwrap(),
}
}
/// accept a new input string
fn input(&mut self, line: &str) -> Result<(), xous::Error> {
self.input = Some(String::from(line));
Ok(())
}
fn msg(&mut self, message: MessageEnvelope) { self.msg = Some(message); }
fn circular_push(&mut self, item: History) {
if self.history.len() >= self.history_len {
self.history.remove(0);
}
self.history.push(item);
}
/// update the loop, in response to various inputs
fn update(&mut self, was_callback: bool, init_done: bool) -> Result<(), xous::Error> {
let debug1 = false;
// if we had an input string, do something
if let Some(local) = &self.input {
let input_history = History { text: local.to_string(), is_input: true };
self.circular_push(input_history);
}
// AT THIS POINT: if we have other inputs, update accordingly
// other inputs might be, for example, events that came in from other servers that would
// side effect our commands
// redraw UI once upon accepting all input
if !was_callback {
// don't need to redraw on a callback, save some cycles
self.redraw(init_done).expect("can't redraw");
}
let mut dirty = true;
// take the input and pass it on to the various command parsers, and attach result
if let Some(local) = &mut self.input {
log::trace!("processing line: {}", local);
if let Some(res) = self.env.dispatch(Some(local), None).expect("command dispatch failed") {
#[cfg(feature = "tts")]
{
let mut output = t!("shellchat.output-tts", locales::LANG).to_string();
output.push_str(res.as_str());
self.tts.tts_simple(&output).unwrap();
}
let output_history = History { text: String::from(res.as_str()), is_input: false };
self.circular_push(output_history);
} else {
dirty = false;
}
} else if let Some(msg) = &self.msg {
log::trace!("processing callback msg: {:?}", msg);
if let Some(res) = self.env.dispatch(None, Some(msg)).expect("callback failed") {
#[cfg(feature = "tts")]
{
let mut output = t!("shellchat.output-tts", locales::LANG).to_string();
output.push_str(res.as_str());
self.tts.tts_simple(&output).unwrap();
}
let output_history = History { text: String::from(res.as_str()), is_input: false };
self.circular_push(output_history);
} else {
dirty = false;
}
}
// clear all the inputs to the loop, so we don't process them twice
self.input = None;
self.msg = None;
// redraw UI now that we've responded
if dirty {
self.redraw(init_done).expect("can't redraw");
}
if debug1 {
for h in self.history.iter() {
info!("command history is_input: {}, text:{}", h.is_input, h.text);
}
}
Ok(())
}
fn clear_area(&self) {
self.gam
.draw_rectangle(
self.content,
Rectangle::new_with_style(
Point::new(0, 0),
self.screensize,
DrawStyle { fill_color: Some(PixelColor::Light), stroke_color: None, stroke_width: 0 },
),
)
.expect("can't clear content area");
}
fn redraw(&mut self, _init_done: bool) -> Result<(), xous::Error> {
log::trace!("going into redraw");
self.clear_area();
#[cfg(target_os = "xous")] // only draw "please wait" if we're not in hosted mode
if !_init_done {
let mut init_tv = TextView::new(
self.content,
TextBounds::CenteredTop(Rectangle::new(
Point::new(0, self.screensize.y / 3 - 64),
Point::new(self.screensize.x, self.screensize.y / 3),
)),
);
init_tv.style = blitstr2::GlyphStyle::Bold;
init_tv.draw_border = false;
write!(init_tv.text, "{}", t!("shellchat.bootwait", locales::LANG)).ok();
self.gam.post_textview(&mut init_tv).expect("couldn't render wait text");
self.gam.redraw().expect("couldn't redraw screen");
}
// this defines the bottom border of the text bubbles as they stack up wards
let mut bubble_baseline = self.screensize.y - self.margin.y;
log::trace!("drawing chat history");
// iterator returns from oldest to newest
// .rev() iterator is from newest to oldest
for h in self.history.iter().rev() {
let mut bubble_tv = if h.is_input {
TextView::new(
self.content,
TextBounds::GrowableFromBr(
Point::new(self.screensize.x - self.margin.x, bubble_baseline),
self.bubble_width,
),
)
} else {
TextView::new(
self.content,
TextBounds::GrowableFromBl(Point::new(self.margin.x, bubble_baseline), self.bubble_width),
)
};
if h.is_input {
bubble_tv.border_width = 1;
} else {
bubble_tv.border_width = 2;
}
bubble_tv.draw_border = true;
bubble_tv.clear_area = true;
bubble_tv.rounded_border = Some(self.bubble_radius);
bubble_tv.style = gam::SYSTEM_STYLE;
bubble_tv.margin = self.bubble_margin;
bubble_tv.ellipsis = false;
bubble_tv.insertion = None;
write!(bubble_tv.text, "{}", h.text.as_str()).expect("couldn't write history text to TextView");
log::trace!("posting {}", bubble_tv.text);
self.gam.post_textview(&mut bubble_tv).expect("couldn't render bubble textview");
if let Some(bounds) = bubble_tv.bounds_computed {
// we only subtract 1x of the margin because the bounds were computed from a "bottom right"
// that already counted the margin once.
bubble_baseline -= (bounds.br.y - bounds.tl.y) + self.bubble_space + self.bubble_margin.y;
if bubble_baseline <= 0 {
// don't draw history that overflows the top of the screen
break;
}
} else {
break; // we get None on the bounds computed if the text view fell off the top of the screen
}
}
log::trace!("shellchat redraw##");
self.gam.redraw().expect("couldn't redraw screen");
// self.gam.request_ime_redraw().expect("couldn't redraw the IME area");
Ok(())
}
}
////////////////// local message passing from Ux Callback
use num_traits::{FromPrimitive, ToPrimitive};
#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive)]
enum ShellOpcode {
/// a line of text has arrived
Line = 0, // make sure we occupy opcodes with discriminants < 1000, as the rest are used for callbacks
/// redraw our UI
Redraw,
/// change focus
ChangeFocus,
/// exit the application
Quit,
}
//////////////////
// nothing prevents the two from being the same, other than naming conventions
pub(crate) const SERVER_NAME_SHELLCHAT: &str = "_Shell chat application_"; // used internally by xous-names
fn main() -> ! {
#[cfg(not(any(feature = "ditherpunk", feature = "locktests")))]
wrapped_main();
#[cfg(any(feature = "ditherpunk", feature = "locktests"))]
let stack_size = 8192 * 1024;
#[cfg(any(feature = "ditherpunk", feature = "locktests"))]
std::thread::Builder::new().stack_size(stack_size).spawn(wrapped_main).unwrap().join().unwrap()
}
fn wrapped_main() -> ! {
log_server::init_wait().unwrap();
log::set_max_level(log::LevelFilter::Info);
info!("my PID is {}", xous::process::id());
let xns = xous_names::XousNames::new().unwrap();
// unlimited connections allowed, this is a user app and it's up to the app to decide its policy
let shch_sid = xns.register_name(SERVER_NAME_SHELLCHAT, None).expect("can't register server");
//log::trace!("registered with NS -- {:?}", shch_sid);
#[cfg(feature = "tts")]
let tts = TtsFrontend::new(&xns).unwrap();
let mut repl = Repl::new(&xns, shch_sid);
let mut update_repl = false;
let mut was_callback = false;
let mut allow_redraw = true;
let pddb_init_done = Arc::new(AtomicBool::new(false));
repl.redraw(pddb_init_done.load(Ordering::SeqCst)).ok();
thread::spawn({
let pddb_init_done = pddb_init_done.clone();
let main_conn = xous::connect(shch_sid).unwrap();
move || {
let pddb = pddb::Pddb::new();
pddb.mount_attempted_blocking();
pddb_init_done.store(true, Ordering::SeqCst);
xous::send_message(
main_conn,
xous::Message::new_scalar(ShellOpcode::Redraw.to_usize().unwrap(), 0, 0, 0, 0),
)
.ok();
}
});
log::trace!("starting main loop");
#[cfg(feature = "autobasis-ci")]
{
log::info!("starting autobasis CI launcher");
autobasis_launcher(shch_sid);
}
loop {
let msg = xous::receive_message(shch_sid).unwrap();
let shell_op: Option<ShellOpcode> = FromPrimitive::from_usize(msg.body.id());
log::debug!("Shellchat got message {:?}", msg);
match shell_op {
Some(ShellOpcode::Line) => {
let buffer = unsafe { Buffer::from_memory_message(msg.body.memory_message().unwrap()) };
let s = buffer.as_flat::<String, _>().unwrap();
log::trace!("shell got input line: {}", s.as_str());
#[cfg(feature = "tts")]
{
let mut input = t!("shellchat.input-tts", locales::LANG).to_string();
input.push_str(s.as_str());
tts.tts_simple(&input).unwrap();
}
repl.input(s.as_str()).expect("REPL couldn't accept input string");
update_repl = true; // set a flag, instead of calling here, so message can drop and calling server is released
was_callback = false;
}
Some(ShellOpcode::Redraw) => {
if allow_redraw {
repl.redraw(pddb_init_done.load(Ordering::SeqCst)).expect("REPL couldn't redraw");
}
}
Some(ShellOpcode::ChangeFocus) => xous::msg_scalar_unpack!(msg, new_state_code, _, _, _, {
let new_state = gam::FocusState::convert_focus_change(new_state_code);
match new_state {
gam::FocusState::Background => {
allow_redraw = false;
}
gam::FocusState::Foreground => {
allow_redraw = true;
}
}
}),
Some(ShellOpcode::Quit) => {
log::error!("got Quit");
break;
}
_ => {
log::trace!("got unknown message, treating as callback");
repl.msg(msg);
update_repl = true;
was_callback = true;
}
}
if update_repl {
repl.update(was_callback, pddb_init_done.load(Ordering::SeqCst))
.expect("REPL had problems updating");
update_repl = false;
}
log::trace!("reached bottom of main loop");
}
// clean up our program
log::error!("main loop exit, destroying servers");
xns.unregister_server(shch_sid).unwrap();
xous::destroy_server(shch_sid).unwrap();
log::trace!("quitting");
xous::terminate_process(0)
}
#[cfg(feature = "autobasis-ci")]
fn autobasis_launcher(sid: xous::SID) {
let _ = std::thread::spawn({
let conn = xous::connect(sid).unwrap();
move || {
let pddb = pddb::Pddb::new();
pddb.is_mounted_blocking();
let tt = ticktimer_server::Ticktimer::new().unwrap();
tt.sleep_ms(5000).unwrap();
let cmd = String::from("pddb btest");
let buf = Buffer::into_buf(cmd).unwrap();
buf.send(conn, ShellOpcode::Line.to_u32().unwrap()).expect("couldn't kick off the CI");
log::info!("CI run started");
}
});
}
Served by rngit 1.4.0 - Generated in 0.04s